"""!

@brief Cluster analysis algorithm: K-Medoids.

@authors Andrei Novikov (pyclustering@yandex.ru)
@date 2014-2020
@copyright BSD-3-Clause

"""


import numpy

from pyclustering.cluster.encoder import type_encoding

from pyclustering.utils.metric import distance_metric, type_metric

import pyclustering.core.pam_build_wrapper as pam_build_wrapper
import pyclustering.core.kmedoids_wrapper as kmedoids_wrapper

from pyclustering.core.wrapper import ccore_library
from pyclustering.core.metric_wrapper import metric_wrapper


class build:
    """!
    @brief PAM BUILD algorithm is designed to find optimal initial medoids for K-Medoids algorithms family, like PAM.

    @details The initialization procedure chooses `k` times the point which yields the smallest distance sum of total
              deviation. Complexity of the algorithm is \f$O\left ( n^{2}k \right )\f$, where `n` is the amount
              of points and `k` is the amount of initial medoids to generate.

    Implementation based on paper @cite inproceedings::cluster::kmedoids::1.

    There is an example where PAM BUILD algorithm is used to generate initial medoids for various samples.
    @code
        from pyclustering.cluster.kmedoids import build
        from pyclustering.cluster import cluster_visualizer
        from pyclustering.utils import read_sample
        from pyclustering.samples.definitions import FCPS_SAMPLES, SIMPLE_SAMPLES

        # Short name of each sample.
        name = ['TwoDiamonds', 'Lsun', 'WingNut', 'Tetra', 'Hepta', 'Simple01', 'Simple02', 'Simple03']

        # Path to each sample.
        path = [FCPS_SAMPLES.SAMPLE_TWO_DIAMONDS, FCPS_SAMPLES.SAMPLE_LSUN, FCPS_SAMPLES.SAMPLE_WING_NUT, FCPS_SAMPLES.SAMPLE_TETRA, FCPS_SAMPLES.SAMPLE_HEPTA, SIMPLE_SAMPLES.SAMPLE_SIMPLE1, SIMPLE_SAMPLES.SAMPLE_SIMPLE2, SIMPLE_SAMPLES.SAMPLE_SIMPLE3]

        # Amount of medoids that should be initialized for each sample.
        amount_medoids = [2, 3, 2, 4, 7, 2, 3, 4]

        # Prepare 8 plots where each line contains 4 plots.
        visualizer = cluster_visualizer(8, 4)

        for i in range(len(name)):
            # Load list of points to cluster analysis.
            sample = read_sample(path[i])

            # Initialize initial medoids using PAM BUILD algorithm.
            initial_medoids = build(sample, amount_medoids[i]).initialize()

            # Set bigger medoid marker for 3-dimensional data to make it more visible.
            medoid_marker_size = 8
            if len(sample[0]) == 3:
                medoid_marker_size = 32

            # Append points as a one big cluster.
            visualizer.append_cluster(sample, canvas=i, marker='.', markersize=2, color='dimgray')

            # Append initial medoids that were obtained by PAM BUILD algorithm.
            visualizer.append_cluster(initial_medoids, sample, canvas=i, markersize=medoid_marker_size, marker='*', color='blue')

            # Set name of the sample that was used.
            visualizer.set_canvas_title(name[i], canvas=i)

        # Display results.
        visualizer.show()
    @endcode

    PAM BUILD algorithm provides much more optimal initial medoids in average than kmeans++ based approach and as a
    result it reduces amount of iterations that is needed for K-Medoids algorithms family to extract clusters. There is
    an illustration that was generated by the code above where initial medoids are marked by blue stars.

    @image html pam_build_initial_medoids.png "Fig. 1. Initial medoids for various samples generated by PAM BUILD algorithm."

    """

    def __init__(self, data, k, ccore=True, **kwargs):
        """!
        @brief Create PAM BUILD algorithm to initialize medoids.

        @param[in] data (array-like): Points where each point is represented by a list of coordinates.
        @param[in] k (uint): The amount of medoids to generate.
        @param[in] ccore (bool): If `True` then C++ implementation is used instead of Python code (by default is `True`).
        @param[in] **kwargs: Arbitrary keyword arguments (available arguments: `data_type`, `metric`).

        <b>Keyword Args:</b><br>
            - metric (distance_metric): Metric that is used for distance calculation between two points.
            - data_type (string): Data type of input sample `data` (`points`, `distance_matrix`).

        """
        self.__data = data
        self.__amount = k

        self.__metric = kwargs.get('metric', distance_metric(type_metric.EUCLIDEAN_SQUARE))
        self.__data_type = kwargs.get('data_type', 'points')

        self.__distance_calculator = self.__create_distance_calculator()
        self.__ccore = ccore and self.__metric.get_type() != type_metric.USER_DEFINED
        if self.__ccore:
            self.__ccore = ccore_library.workable()

        self.__verify_arguments()


    def initialize(self):
        """!
        @brief Performs PAM BUILD procedure to find initial medoids.

        @return (list) List of initial medoids, for example, `[2, 4, 15, 78]` where each element corresponds to index in
                        the input data set.

        """

        if self.__ccore is True:
            return self.__initialize_by_ccore()

        return self.__initialize_by_python()


    def __initialize_by_ccore(self):
        """!
        @brief Performs processing using C/C++ pyclustering library.

        """
        ccore_metric = metric_wrapper.create_instance(self.__metric)
        return pam_build_wrapper.pam_build(self.__data, self.__amount, ccore_metric.get_pointer(), self.__data_type)


    def __initialize_by_python(self):
        """!
        @brief Performs processing using Python code.

        """
        self.__medoids = []
        self.__distance_closest_medoid = [0] * len(self.__data)

        self.__calculate_first_medoid()
        while len(self.__medoids) != self.__amount:
            self.__calculate_next_medoid()

        return self.__medoids


    def __create_distance_calculator(self):
        """!
        @brief Creates distance calculator in line with algorithms parameters.

        @return (callable) Distance calculator.

        """
        if self.__data_type == 'points':
            return lambda index1, index2: self.__metric(self.__data[index1], self.__data[index2])

        elif self.__data_type == 'distance_matrix':
            if isinstance(self.__data, numpy.matrix):
                return lambda index1, index2: self.__data.item((index1, index2))

            return lambda index1, index2: self.__data[index1][index2]


    def __calculate_first_medoid(self):
        optimal_deviation = float('inf')

        current_distances = [0] * len(self.__data)

        for i in range(len(self.__data)):
            total_deviation = 0.0
            for j in range(len(self.__data)):
                if i == j:
                    current_distances[j] = 0
                    continue

                distance = self.__distance_calculator(i, j)
                total_deviation += distance
                current_distances[j] = distance

            if total_deviation < optimal_deviation:
                self.__medoids = [i]
                optimal_deviation = total_deviation
                self.__distance_closest_medoid, current_distances = current_distances, self.__distance_closest_medoid


    def __calculate_next_medoid(self):
        optimal_medoid = -1
        optimal_deviation = float('inf')
        optimal_distances = [0] * len(self.__data)
        current_distances = [0] * len(self.__data)

        for i in range(len(self.__data)):
            if i in self.__medoids:
                continue

            total_deviation = 0.0
            for j in range(len(self.__data)):
                if (i == j) or (j in self.__medoids):
                    current_distances[j] = 0
                    continue

                distance = min(self.__distance_calculator(i, j), self.__distance_closest_medoid[j])
                total_deviation += distance
                current_distances[j] = distance

            if total_deviation < optimal_deviation:
                optimal_medoid = i
                optimal_deviation = total_deviation
                optimal_distances, current_distances = current_distances, optimal_distances

        self.__medoids.append(optimal_medoid)
        self.__distance_closest_medoid = optimal_distances


    def __verify_arguments(self):
        if self.__data is None:
            raise ValueError("Input data is None.")

        if len(self.__data) == 0:
            raise ValueError("Input data is empty (size: '%d')." % len(self.__data))

        if self.__amount < 1:
            raise ValueError("K value (amount of initial medoids to generate) should be equal or greater than 1 "
                             "(current value: '%d')." % self.__amount)

        if self.__amount > len(self.__data):
            raise ValueError("K value (amount of initial medoids to generate) should be equal or less than data size "
                             "(current value: '%d', data size: '%d')." % (self.__amount, len(self.__data)))



class kmedoids:
    """!
    @brief Class represents clustering algorithm K-Medoids (PAM algorithm).
    @details PAM is a partitioning clustering algorithm that uses the medoids instead of centers like in case of K-Means
              algorithm. Medoid is an object with the smallest dissimilarity to all others in the cluster. PAM algorithm
              complexity is \f$O\left ( k\left ( n-k \right )^{2} \right )\f$.

              The performance of the algorithm depends on initial medoids and it is recommended to use BUILD PAM
              initialization algorithm for that purpose. K-Means++ is also could be considered as an initialization
              algorithm with smaller complexity `O(nk)`, but produces a worse starting point.

    Implementation based on paper @cite inproceedings::cluster::kmedoids::1.

    There is an example where PAM algorithm is used to cluster `Tetra` data where PAM BUILD algorithm is used to
    initialize medoids:
    @code
        from pyclustering.cluster.kmedoids import kmedoids, build
        from pyclustering.cluster import cluster_visualizer
        from pyclustering.utils import read_sample
        from pyclustering.samples.definitions import FCPS_SAMPLES

        # Load list of points `Tetra` for cluster analysis.
        sample = read_sample(FCPS_SAMPLES.SAMPLE_TETRA)

        # Initialize initial medoids using PAM BUILD algorithm
        initial_medoids = build(sample, 4).initialize()

        # Create instance of K-Medoids (PAM) algorithm.
        kmedoids_instance = kmedoids(sample, initial_medoids)

        # Run cluster analysis
        kmedoids_instance.process()

        # Display clustering results to console.
        print("Clusters:", kmedoids_instance.get_clusters())
        print("Labels:", kmedoids_instance.get_labels())
        print("Medoids:", kmedoids_instance.get_medoids())
        print("Total Deviation:", kmedoids_instance.get_total_deviation())
        print("Iterations:", kmedoids_instance.get_iterations())

        # Display clustering results.
        visualizer = cluster_visualizer()
        visualizer.append_clusters(kmedoids_instance.get_clusters(), sample)
        visualizer.append_cluster(initial_medoids, sample, markersize=120, marker='*', color='dimgray')
        visualizer.append_cluster(kmedoids_instance.get_medoids(), sample, markersize=120, marker='*', color='black')
        visualizer.show()
    @endcode

    The following clustering results are expected in a console:
    @code
        Clusters: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199], [200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299], [300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399]]
        Labels: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
        Medoids: [0, 100, 201, 300]
        Total Deviation: 239.25250279395595
        Iterations: 3
    @endcode

    And the following visualization is expected:
    @image html pam_clustering_tetra.png "Fig. 1. K-Medoids (PAM) clustering results `Tetra` (PAM BUILD initialization)."

    There is an example where PAM algorithm is used to cluster `TwoDiamonds` data where K-Means++ algorithm is used
    to initialize medoids:
    @code
        from pyclustering.cluster.kmedoids import kmedoids
        from pyclustering.cluster.center_initializer import kmeans_plusplus_initializer
        from pyclustering.cluster import cluster_visualizer
        from pyclustering.utils import read_sample
        from pyclustering.samples.definitions import FCPS_SAMPLES

        # Load list of points for cluster analysis.
        sample = read_sample(FCPS_SAMPLES.SAMPLE_TWO_DIAMONDS)

        # Initialize initial medoids using K-Means++ algorithm
        initial_medoids = kmeans_plusplus_initializer(sample, 2).initialize(return_index=True)

        # Create instance of K-Medoids (PAM) algorithm.
        kmedoids_instance = kmedoids(sample, initial_medoids)

        # Run cluster analysis and obtain results.
        kmedoids_instance.process()
        clusters = kmedoids_instance.get_clusters()
        medoids = kmedoids_instance.get_medoids()

        # Print allocated clusters.
        print("Clusters:", clusters)

        # Display clustering results.
        visualizer = cluster_visualizer()
        visualizer.append_clusters(clusters, sample)
        visualizer.append_cluster(initial_medoids, sample, markersize=12, marker='*', color='gray')
        visualizer.append_cluster(medoids, sample, markersize=14, marker='*', color='black')
        visualizer.show()
    @endcode

    @image html pam_clustering_two_diamonds.png "Fig. 2. K-Medoids (PAM) clustering results `TwoDiamonds` (K-Means++ initialization)."

    Metric for calculation distance between points can be specified by parameter additional `metric`:
    @code
        # create Minkowski distance metric with degree equals to '2'
        metric = distance_metric(type_metric.MINKOWSKI, degree=2)

        # create K-Medoids algorithm with specific distance metric
        kmedoids_instance = kmedoids(sample, initial_medoids, metric=metric)

        # run cluster analysis and obtain results
        kmedoids_instance.process()
        clusters = kmedoids_instance.get_clusters()
    @endcode

    Distance matrix can be used instead of sequence of points to increase performance and for that purpose parameter `data_type` should be used:
    @code
        # calculate distance matrix for sample
        sample = read_sample(path_to_sample)
        matrix = calculate_distance_matrix(sample)

        # create K-Medoids algorithm for processing distance matrix instead of points
        kmedoids_instance = kmedoids(matrix, initial_medoids, data_type='distance_matrix')

        # run cluster analysis and obtain results
        kmedoids_instance.process()

        clusters = kmedoids_instance.get_clusters()
        medoids = kmedoids_instance.get_medoids()
    @endcode

    @see pam_build

    """
    
    
    def __init__(self, data, initial_index_medoids, tolerance=0.0001, ccore=True, **kwargs):
        """!
        @brief Constructor of clustering algorithm K-Medoids (PAM).
        
        @param[in] data (list): Input data that is presented as list of points (objects), each point should be represented by list or tuple.
        @param[in] initial_index_medoids (list): Indexes of intial medoids (indexes of points in input data).
        @param[in] tolerance (double): Stop condition: if maximum value of distance change of medoids of clusters is less than tolerance than algorithm will stop processing.
        @param[in] ccore (bool): If `True` then C++ implementation is used instead of Python code.
        @param[in] **kwargs: Arbitrary keyword arguments (available arguments: `metric`, `data_type`, `itermax`).

        <b>Keyword Args:</b><br>
            - metric (distance_metric): Metric that is used for distance calculation between two points.
            - data_type (string): Data type of input sample `data` that is processed by the algorithm (`points`, `distance_matrix`).
            - itermax (uint): Maximum number of iteration for cluster analysis.

        """
        self.__pointer_data = data
        self.__clusters = []
        self.__medoid_indexes = initial_index_medoids[:]
        self.__labels = [-1] * len(data)
        self.__distance_first_medoid = [float('inf')] * len(data)
        self.__distance_second_medoid = [float('inf')] * len(data)
        self.__tolerance = tolerance
        self.__iterations = 0
        self.__total_deviation = float('inf')

        self.__metric = kwargs.get('metric', distance_metric(type_metric.EUCLIDEAN_SQUARE))
        self.__data_type = kwargs.get('data_type', 'points')
        self.__itermax = kwargs.get('itermax', 200)
        self.__initializer = kwargs.get('initializer', 'build')

        self.__distance_calculator = self.__create_distance_calculator()

        self.__ccore = ccore and self.__metric.get_type() != type_metric.USER_DEFINED
        if self.__ccore:
            self.__ccore = ccore_library.workable()

        self.__verify_arguments()


    def process(self):
        """!
        @brief Performs cluster analysis in line with rules of K-Medoids algorithm.

        @return (kmedoids) Returns itself (K-Medoids instance).

        @remark Results of clustering can be obtained using corresponding get methods.
        
        @see get_clusters()
        @see get_medoids()
        
        """
        
        if self.__ccore is True:
            ccore_metric = metric_wrapper.create_instance(self.__metric)
            self.__clusters, self.__medoid_indexes, self.__iterations, self.__total_deviation = kmedoids_wrapper.kmedoids(self.__pointer_data, self.__medoid_indexes, self.__tolerance, self.__itermax, ccore_metric.get_pointer(), self.__data_type)

            # TODO: calculate it on C++ side as well
            for index_cluster in range(len(self.__clusters)):
                for index_point in self.__clusters[index_cluster]:
                    self.__labels[index_point] = index_cluster

        else:
            changes = float('inf')
            previous_deviation, self.__total_deviation = float('inf'), 0

            self.__iterations = 0

            if self.__itermax > 0:
                self.__total_deviation = self.__update_clusters()

            while (changes > self.__tolerance) and (self.__iterations < self.__itermax):
                self.__iterations += 1
                swap_cost = self.__swap_medoids()

                if swap_cost != float('inf'):
                    previous_deviation = self.__total_deviation
                    self.__total_deviation = self.__update_clusters()
                    changes = previous_deviation - self.__total_deviation
                else:
                    break



            self.__erase_empty_clusters()

        return self


    def predict(self, points):
        """!
        @brief Calculates the closest cluster to each point.

        @param[in] points (array_like): Points for which closest clusters are calculated.

        @return (list) List of closest clusters for each point. Each cluster is denoted by index. Return empty
                 collection if 'process()' method was not called.

        An example how to calculate (or predict) the closest cluster to specified points.
        @code
            from pyclustering.cluster.kmedoids import kmedoids
            from pyclustering.samples.definitions import SIMPLE_SAMPLES
            from pyclustering.utils import read_sample

            # Load list of points for cluster analysis.
            sample = read_sample(SIMPLE_SAMPLES.SAMPLE_SIMPLE3)

            # Initial medoids for sample 'Simple3'.
            initial_medoids = [4, 12, 25, 37]

            # Create instance of K-Medoids algorithm with prepared centers.
            kmedoids_instance = kmedoids(sample, initial_medoids)

            # Run cluster analysis.
            kmedoids_instance.process()

            # Calculate the closest cluster to following two points.
            points = [[0.35, 0.5], [2.5, 2.0]]
            closest_clusters = kmedoids_instance.predict(points)
            print(closest_clusters)
        @endcode

        """

        if len(self.__clusters) == 0:
            return []

        medoids = [self.__pointer_data[index] for index in self.__medoid_indexes]
        differences = numpy.zeros((len(points), len(medoids)))
        for index_point in range(len(points)):
            differences[index_point] = [self.__metric(points[index_point], center) for center in medoids]

        return numpy.argmin(differences, axis=1)


    def get_clusters(self):
        """!
        @brief Returns list of allocated clusters, each cluster contains indexes of objects in list of data.
        
        @see process()
        @see get_labels()
        @see get_medoids()
        
        """
        
        return self.__clusters


    def get_labels(self):
        """!
        @brief Return list of labels, for example, [0, 1, 1, 1, 0] means that the first and the last points belong to
                cluster with index `0`, and points in between belong to cluster `1`.

        @see process()
        @see get_clusters()
        @see get_medoids()

        """

        return self.__labels


    def get_medoids(self):
        """!
        @brief Returns list of medoids of allocated clusters represented by indexes from the input data.
        
        @see process()
        @see get_clusters()
        @see get_labels()
        
        """

        return self.__medoid_indexes


    def get_iterations(self):
        """!
        @brief Returns the amount of iterations that were performed during the clustering process.
        @return (uint) The amount of iterations that were performed during the clustering process.

        @see process()

        """
        return self.__iterations


    def get_total_deviation(self):
        """!
        @brief Returns total deviation - the final loss after optimization.
        @return (float) The total deviation - the final loss after optimization.

        @see process()

        """
        return self.__total_deviation


    def get_cluster_encoding(self):
        """!
        @brief Returns clustering result representation type that indicate how clusters are encoded.
        
        @return (type_encoding) Clustering result representation.
        
        @see get_clusters()
        
        """
        
        return type_encoding.CLUSTER_INDEX_LIST_SEPARATION


    def __verify_arguments(self):
        """!
        @brief Verify input parameters for the algorithm and throw exception in case of incorrectness.

        """
        if len(self.__pointer_data) == 0:
            raise ValueError("Input data is empty (size: '%d')." % len(self.__pointer_data))

        if len(self.__medoid_indexes) == 0:
            raise ValueError("Initial medoids are empty (size: '%d')." % len(self.__pointer_data))

        if self.__tolerance < 0:
            raise ValueError("Tolerance (current value: '%d') should be greater or equal to 0." %
                             self.__tolerance)

        if self.__itermax < 0:
            raise ValueError("Maximum iterations (current value: '%d') should be greater or equal to 0." %
                             self.__tolerance)


    def __create_distance_calculator(self):
        """!
        @brief Creates distance calculator in line with algorithms parameters.

        @return (callable) Distance calculator.

        """
        if self.__data_type == 'points':
            return lambda index1, index2: self.__metric(self.__pointer_data[index1], self.__pointer_data[index2])

        elif self.__data_type == 'distance_matrix':
            if isinstance(self.__pointer_data, numpy.matrix):
                return lambda index1, index2: self.__pointer_data.item((index1, index2))

            return lambda index1, index2: self.__pointer_data[index1][index2]

        else:
            raise TypeError("Unknown type of data is specified '%s'" % self.__data_type)


    def __update_clusters(self):
        """!
        @brief Calculate distance to each point from the each cluster.
        @details Nearest points are captured by according clusters and as a result clusters are updated.

        @return (double) Total deviation (distance from each point to its closest medoid).

        """

        total_deviation = 0.0
        self.__clusters = [[] for i in range(len(self.__medoid_indexes))]
        for index_point in range(len(self.__pointer_data)):
            index_optim = -1
            dist_optim_first = float('Inf')
            dist_optim_second = float('Inf')
            
            for index in range(len(self.__medoid_indexes)):
                dist = self.__distance_calculator(index_point, self.__medoid_indexes[index])

                if dist < dist_optim_first:
                    dist_optim_second = dist_optim_first
                    index_optim = index
                    dist_optim_first = dist
                elif dist < dist_optim_second:
                    dist_optim_second = dist

            total_deviation += dist_optim_first
            self.__clusters[index_optim].append(index_point)
            self.__labels[index_point] = index_optim

            self.__distance_first_medoid[index_point] = dist_optim_first
            self.__distance_second_medoid[index_point] = dist_optim_second

        return total_deviation


    def __swap_medoids(self):
        """!
        @brief Swap existed medoid with non-medoid points in order to find the most optimal medoid.

        @return (double) Cost that is needed to swap two medoids.

        """

        optimal_swap_cost = float('inf')
        optimal_index_cluster = None
        optimal_index_medoid = None

        for index_cluster in range(len(self.__clusters)):
            for candidate_medoid_index in range(len(self.__pointer_data)):
                if (candidate_medoid_index in self.__medoid_indexes) or (self.__distance_first_medoid[candidate_medoid_index] == 0.0):
                    continue

                swap_cost = self.__calculate_swap_cost(candidate_medoid_index, index_cluster)
                if swap_cost < optimal_swap_cost:
                    optimal_swap_cost = swap_cost
                    optimal_index_cluster = index_cluster
                    optimal_index_medoid = candidate_medoid_index

        if optimal_index_cluster is not None:
            self.__medoid_indexes[optimal_index_cluster] = optimal_index_medoid

        return optimal_swap_cost


    def __calculate_swap_cost(self, index_candidate, cluster_index):
        """!
        @brief Calculates cost to swap `index_candidate` with the current medoid `cluster_index`.

        @param[in] index_candidate (uint): Index point that is considered as a medoid candidate.
        @param[in] cluster_index (uint): Index of a cluster where the current medoid is used for calculation.

        @return (double) Cost that is needed to swap medoids.

        """
        cost = 0.0

        for index_point in range(len(self.__pointer_data)):
            if index_point == index_candidate:
                continue

            candidate_distance = self.__distance_calculator(index_point, index_candidate)
            if self.__labels[index_point] == cluster_index:
                cost += min(candidate_distance, self.__distance_second_medoid[index_point]) - self.__distance_first_medoid[index_point]
            elif candidate_distance < self.__distance_first_medoid[index_point]:
                cost += candidate_distance - self.__distance_first_medoid[index_point]

        return cost - self.__distance_first_medoid[index_candidate]


    def __erase_empty_clusters(self):
        """!
        @brief Erase empty clusters and their medoids.
        @details Data might have identical points and a lot of identical points and as a result medoids might correspond
                  to points that are totally identical.

        """

        erase_required = False

        # Before processing check if there are empty clusters
        for cluster in self.__clusters:
            if len(cluster) == 0:
                erase_required = True
                break

        if erase_required is False:
            return

        none_empty_clusters = []
        none_empty_medoids = []

        for index in range(len(self.__clusters)):
            if len(self.__clusters[index]) == 0:
                continue

            none_empty_clusters.append(self.__clusters[index])
            none_empty_medoids.append(self.__medoid_indexes[index])

        self.__clusters = none_empty_clusters
        self.__medoid_indexes = none_empty_medoids


class pam(kmedoids):
    """!
    @brief Class represents clustering algorithm PAM algorithm (alias of K-Medoids algorithm).

    @see kmedoids

    """
